/**
 *    Copyright (C) 2008, 2009  Victor Smirnov <aist11@gmail.com>
 *
 *    This program is free software; you can redistribute it and/or modify
 *    it under the terms of the GNU General Public License as published by
 *    the Free Software Foundation; either version 3 of the License, or
 *    (at your option) any later version.
 *
 *    This program is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *    GNU General Public License for more details.
 *
 *    You should have received a copy of the GNU General Public License along
 *    with this program; if not, write to the Free Software Foundation, Inc.,
 *    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 *    See LICENSE.exceptions in the root folder of this project for possible
 *    exceptions
 *
 */


package ru.teleology.cubefs.api.reflection;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import ru.teleology.cubefs.api.models.Container;
import ru.teleology.cubefs.api.reflection.Types.ArgumentTransformer;
import ru.teleology.cubefs.api.reflection.list.List;
import ru.teleology.cubefs.api.reflection.list.ListOfTypes;
import ru.teleology.cubefs.api.reflection.number.Number;
import ru.teleology.cubefs.api.reflection.value.BooleanValue;
import ru.teleology.cubefs.api.reflection.value.ConstStringValue;
import ru.teleology.cubefs.api.reflection.value.IDValue;
import ru.teleology.cubefs.api.reflection.value.StringRefValue;
import ru.teleology.cubefs.api.reflection.value.StringValue;
import ru.teleology.cubefs.api.reflection.value.TypedIDValue;
import ru.teleology.cubefs.api.reflection.value.Version;
import ru.teleology.cubefs.api.reflection.value.VersionValue;

@NativeType(name = Types.TYPE_TYPE)
public class Type extends Typed {
	
	private Map<Method, OperationDescriptor> mapping = new HashMap<Method, OperationDescriptor>();
	private Class<?> operations;
	private Class<?> staticOperations;
	
	public Type(long handle) {
		super(handle, false);		
	}
	
	protected void buildProxies() {
		Class<?> clazz = findJavaClass(this);
		if (clazz != null) {
			try {
				operations = Class.forName(clazz.getName()+"$Operations");
				
				if (clazz == Container.class) {
					int a =0;
					a++;
				}
				
				buildOperationMapping(operations, false);
			} 
			catch (ClassNotFoundException e) {}
			
			try {
				
				if (clazz == Container.class) {
					int a =0;
					a++;
				}
				
				staticOperations = Class.forName(clazz.getName()+"$StaticOperations");
				buildOperationMapping(staticOperations, true);
			} 
			catch (ClassNotFoundException e) {}
		}
		
		if (parent() != null) parent().buildProxies();
	}
	
	public Object proxy(final Typed target) {
		return proxy(operations, target);
	}
	
	public Object staticProxy() {
		return proxy(staticOperations, null);
	}
	
	protected Object proxy(Class<?> operations, final Typed target) {
		return operations == null ? null :  Proxy.newProxyInstance(Type.class.getClassLoader(), new Class[]{operations}, new InvocationHandler() {	
			public Object invoke(Object proxy, Method method, Object[] args)
					throws Throwable {
				OperationDescriptor op = Type.this.mapping.get(method);
				if (op == null) {
					for (Map.Entry<Method, OperationDescriptor> entry: Type.this.mapping.entrySet()) 
					{
						Operation oop = entry.getValue().getOp();
						System.err.println(oop);
					}
					
					throw new RuntimeException("Can't find native opertaion for " + method);
				}
				
				List<Typed> arguments = null;
				
				if (args != null && args.length != 0) 
				{
					arguments = CoreFactory.GetCoreFactory().CreateStaticListOfTyped();
					for (int c = 0; c < args.length; c++) 
					{
						Typed value = null;
						if (args[c] != null) {
							value = op.getTransformers()[c].transform(args[c]);
						}
						arguments.Add(value);
					}
				}
				
				try {
					Typed result = op.getOp().invoke(target, arguments);
					if (result != null) 
					{
						boolean destroy = false;
						try {
							if (op.rtnConvert == OperationDescriptor.NUMBER) 
							{
								destroy = true;
								return ((Number)result).toObject(op.returnType);
							}
							else if (op.rtnConvert == OperationDescriptor.TYPED) 
							{
								return result;
							}
							else if (op.rtnConvert == OperationDescriptor.BOOLEAN) 
							{
								return ((BooleanValue)result).GetValue();
							}
							else if (op.rtnConvert == OperationDescriptor.ID_VALUE) 
							{
								return ((TypedIDValue)result).GetValue();
							}
							else if (op.rtnConvert == OperationDescriptor.VERSION) 
							{
								return ((VersionValue)result).GetValue();
							}
							else if (op.rtnConvert == OperationDescriptor.STRING) 
							{
								if (result.type() == Types.String) 
								{
									return ((StringValue)result).GetValue();
								}
								else if (result.type() == Types.ConstString) 
								{
									return ((ConstStringValue)result).GetValue();
								}
								else if (result.type() == Types.StringRef) 
								{
									return ((StringRefValue)result).GetValue();
								}
								else {
									throw new RuntimeException("Can't convert return value of "+result.type()+" to String");
								}
							}
							else {
								throw new RuntimeException("Unrecognized return value convertion type: "+op.rtnConvert);
							}
						}
						finally {
							if (op.rtnConvert != OperationDescriptor.TYPED && !result.managed()) 
							{
								result.destroy();
							}
						}
					}
					else {
						return null;
					}
				}
				finally {
					if (arguments!=null) {
						arguments.destroy();
					}
				}
			}			
		});
	}
	
	public String name() {
		return name(getHandle());
	}
	
	public int GetOperationsCount() {
		return GetOperationsCount(getHandle());
	}
	
	public Operation GetOperation(int idx) {
		long h = GetOperation(getHandle(), idx);
		return Types.getInstance(h, Operation.class);
	}
	
	public Operation FindOperation(String name, ListOfTypes arguments) {
		long h = FindOperation(getHandle(), name, arguments!=null ? arguments.getHandle() : 0);
		if (h == 0) {
			return null;
		}
		else return Types.getInstance(h, Operation.class);
	}
	
	public boolean IsSameOrSubclass(Type other) {
		return IsSameOrSubclass(getHandle(), other.getHandle());
	}
	
	public Type parent() {
		long h = parent(getHandle());
		return h != 0 ? Types.getInstance(h, Type.class) : null;
	}
	
	@Override
	public String toString() {
		return "Type "+name();
	}
	
	protected void buildOperationMapping(Class<?> intrface, boolean isStatic) {
		Set<Method> methods = new HashSet<Method>();
		
		for (Method m: intrface.getMethods()) {
			methods.add(m);
		}
		
		for (int c = 0; c < this.GetOperationsCount(); c++) {
			Operation op = GetOperation(c);
			for (Method m: methods) {
				
				Class<?>[] parameterTypes = m.getParameterTypes();
				if (m.getName().equals(op.name()) && parameterTypes.length == op.GetArgumentsCount() && op.IsStatic() == isStatic) 
				{
					if (Types.isAssignableFrom(m.getReturnType(), op.GetReturnType())) 
					{
						boolean match = true;
						for (int d = 0; d < parameterTypes.length; d++) 
						{
							if (!Types.isAssignableFrom(parameterTypes[d], op.GetArgument(d).GetParameterType())) 
							{
								match = false;
								break;
							}
						}
						
						if (match) 
						{
							ArgumentTransformer[] transformers = new ArgumentTransformer[parameterTypes.length]; 
							for (int d = 0; d < parameterTypes.length; d++) 
							{
								Class<?> paramType = parameterTypes[d].isPrimitive() ? Types.getObjectType(parameterTypes[d]) : parameterTypes[d];
								transformers[d] = Types.getTransformer(paramType);
							}
							
							Class<?> rtnType = m.getReturnType().isPrimitive() ? Types.getObjectType(m.getReturnType()) : m.getReturnType(); 
							
							int rtnConvert;
							
							if (Typed.class.isAssignableFrom(m.getReturnType())) {
								rtnConvert = OperationDescriptor.TYPED;
							}
							else if (String.class == m.getReturnType()) {
								rtnConvert = OperationDescriptor.STRING;
							}
							else if (m.getReturnType() == IDValue.class) {
								rtnConvert = OperationDescriptor.ID_VALUE;
							}
							else if (m.getReturnType() == Version.class) {
								rtnConvert = OperationDescriptor.VERSION;
							}
							else if (m.getReturnType() == boolean.class) {
								rtnConvert = OperationDescriptor.BOOLEAN;
							}
							else if (m.getReturnType() == Boolean.class) {
								rtnConvert = OperationDescriptor.BOOLEAN;
							}
							else if (m.getReturnType().isPrimitive()) {
								rtnConvert = OperationDescriptor.NUMBER;
							}
							else {
								rtnConvert = OperationDescriptor.NUMBER;
							}
							
							mapping.put(m, new OperationDescriptor(op, transformers, rtnType, rtnConvert));
						}
					}
				}
			}
		}
	}

	private static native long parent(long me);
	private static native String name(long me);
	private static native int GetOperationsCount(long me);
	private static native long GetOperation(long me, int index);
	private static native boolean IsSameOrSubclass(long me, long other);
	private static native long FindOperation(long me, String name, long arguments);
	
	private static class OperationDescriptor {
		
		public static final int NUMBER 	= 0;
		public static final int STRING 	= 1;
		public static final int TYPED 	= 2;
		public static final int ID_VALUE 	= 3;
		public static final int BOOLEAN 	= 4;
		public static final int VERSION 	= 5;
		
		final Operation op;
		final ArgumentTransformer[] transformers;
		final Class<?> returnType;
		private final int rtnConvert;
		
		OperationDescriptor(Operation op, ArgumentTransformer[] transformers, Class<?> returnType, int rtnConvert) {
			this.op = op;
			this.transformers = transformers;
			this.returnType = returnType;
			this.rtnConvert = rtnConvert;
		}

		public Operation getOp() {
			return op;
		}

		public ArgumentTransformer[] getTransformers() {
			return transformers;
		}
	}	
}